package org.django4j;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Django4j会从'django4j-urls.properties'中获得url与controler之间的映射关系。
 * {@link org.django4j.DispatchServlet}在init的时候会找到django4j-urls.properties，
 * 解析这个文件获得所有的映射关系， UrlContext就是用来表示所有的映射关系。 
 * UrlsContext会被DispatchServlet缓存下来，避免每次都从文件重新解析。
 * 一个映射关系包括以下元素：url正则表达式，controller类，controller方法，request method。
 * jdango4j-urls.properties中的每行内容需要符合以下格式：
 * {全限定的controller类名}.{controller 方法}={正则表达式}
 * 其中正则表达式必须声明为以'GET,POST,PUT,DELETE'中的一种结尾，因为Django4j会将请求
 * url+requestMethod来与正则表达式进行匹配。比如：
 * org.django4j.user.UserController.updateUser=^user/(\d+)/POST$
 * 
 * 注意， 在django4j-urls.properties中正则表达式做作为key/value的value，但是解析到
 * UrlsContext之后，会实用正则表达式作为key， 因为但是正则表达式中可能包含=号，如果在
 * django4j-urls.properties中作为key，会导致异常情况。
 */
public class UrlsContext {
    public static enum RequestMethod {GET, POST, PUT, DELETE};
    
    private final static String DELIM_METHOD = ".";
    private final Log logger = LogFactory.getLog(UrlsContext.class); 
    private List<UrlMapping> urlMappings = new ArrayList<UrlMapping>();
    
    /**
     * Build UrlContext from input stream of jdango4j-urls.properties
     */
    public UrlsContext(InputStream is){
        Properties prop = new Properties();
        try{
            prop.load(is);
            this.parse(prop);
        }
        catch(IOException e){
            throw new Django4jException(e);
        }
    }
    
    /**
     * parse django4j-urls.properties.
     */
    private void parse(Properties prop){
        Iterator iterator = prop.keySet().iterator();
        while (iterator.hasNext()){
            String key = (String)iterator.next();
            String value = (String)prop.get(key);
            UrlMapping mapping = this.parseEntry(key, value);
            urlMappings.add(mapping);
        }
    }
    
    /**
     * parse entry of django4j-urls.properties
     * @param key   The controller+method
     * @param value The regexp+requestMethod
     */
    private UrlMapping parseEntry(String key, String value){
        // parse controller class and method
        int index = key.lastIndexOf(DELIM_METHOD);
        if (index == -1){
            throw new Django4jException(
                    "want a method definition in entry(key=" + key + ").");
        }
        String className = key.substring(0, index);
        String methodName = key.substring(index + DELIM_METHOD.length());
        
        if (logger.isDebugEnabled()){
            logger.debug("parse entry(key=" + key + ") to (className=" 
                    + className + ", methodName=" + methodName + ", regexp=" 
                    + value + ").");
        }
        
        Class controller = this.loadClass(className);
        Method method = this.getMethod(controller, methodName);
        Pattern pattern = null;
        try{
            pattern = Pattern.compile(value);
        }
        catch(Exception e){
            throw new Django4jException(e);
        }

        return new UrlMapping(value, pattern, controller,method);
    }
    
    private Class loadClass(String className){
        Class clazz = null;
        try{
            clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
        }
        catch(Exception e){
            try{
                clazz = Class.forName(className);
            }
            catch(Exception ex){
                throw new Django4jException(ex);
            }
        }
        if (logger.isDebugEnabled()){
            logger.debug("load class(" + className + ") successfully.");
        }        
        return clazz;
    }
    
    private Method getMethod(Class clazz, String methodName){
        Method targetMethod = null;
        Method methods[] = clazz.getMethods();
        for (Method method : methods){
            if (methodName.equals(method.getName())){
                targetMethod = method;
                break;
            }
        }
        if (targetMethod == null){
            throw new Django4jException("can NOT find method(name=" + methodName 
                    + ") in controller class(name=" + clazz.getName() + ").");
        }
        // check return type of method
        Class returnType = targetMethod.getReturnType();
        // even the return type of method can NOT be void.class
        if (!Model.class.isAssignableFrom(returnType)){
            throw new Django4jException(clazz.getName() + "." + methodName 
                    + ": the return type must be assignable from " 
                    + Model.class.getName()+ ", not " + returnType.getName());
        }
        // check the parameters of controller method
        Class paraTypes[] = targetMethod.getParameterTypes();
        if (paraTypes.length < 2){
            throw new Django4jException(clazz.getName() + "." + methodName 
                    + ": there should be at least 2 parameters(request,response..).");
        }
        if (!HttpServletRequest.class.equals(paraTypes[0])){
            throw new Django4jException(clazz.getName() + "." + methodName 
                    + ": first parameter must be HttpServletRequest.");            
        }
        if (!HttpServletResponse.class.equals(paraTypes[1])){
            throw new Django4jException(clazz.getName() + "." + methodName 
                    + ": first parameter must be HttpServletResponse.");             
        }
        // check if the type of other arguments is supported
        if (paraTypes.length > 2){
            for (int i=2; i<paraTypes.length; i++){
                if (logger.isDebugEnabled()){
                    logger.debug(clazz.getName() + "." + methodName + ": args[" 
                            + i + "] is " + paraTypes[i]);
                }
                if (!TypedArgument.isSupported(paraTypes[i])){
                    throw new Django4jException(clazz.getName() + "." + methodName
                            + ": the argument type(" + paraTypes[i] + ") is not supported.");
                }
            }
        }
        
        if (logger.isDebugEnabled()){
            logger.debug("load method(" + methodName + "@" + clazz.getName()
                    + ") successfully");
        }
        return targetMethod;
    }
    
    /**
     * get url mapping definitions.
     */
    public List<UrlMapping> getUrlMappings(){
        return this.urlMappings;
    }
}
